using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Comms;
using Engine;
using Game;

namespace RuthlessConquest {
    public static class ServersManager {
        public static Peer Peer;
        public static Dictionary<IPEndPoint, string> ServerAddresses = [];
        public static double LastLocalDiscoveryTime = double.MinValue;
        public static double LastInternetDiscoveryTime = double.MinValue;
        public static double LocalDiscoveryPeriod;
        public static double InternetDiscoveryPeriod;
        public static DynamicArray<ServerDescription> publicDiscoveredServers = [];
        public static Dictionary<IPEndPoint, double> GameListRequestTimes = new();
        public static double GameListLocalRequestTime;
        public const string fName = "RCServersManager";
        public static bool IsDiscoveryStarted { get; set; }
        public static ReadOnlyList<ServerDescription> DiscoveredServers => new(publicDiscoveredServers);
        public static Version? NewVersionServerDiscovered { get; set; }

        public static void StartServerDiscovery(double internetDiscoveryPeriod = 3.0, double localDiscoveryPeriod = 1.0) {
            LocalDiscoveryPeriod = localDiscoveryPeriod;
            InternetDiscoveryPeriod = internetDiscoveryPeriod;
            if (!IsDiscoveryStarted) {
                IsDiscoveryStarted = true;
                try {
                    CreatePeer();
                    ServerAddresses.Clear();
                    publicDiscoveredServers.Clear();
                    NewVersionServerDiscovered = null;
                    LastLocalDiscoveryTime = double.MinValue;
                    LastInternetDiscoveryTime = double.MinValue;
                }
                catch (Exception arg) {
                    Log.Warning(string.Format(LanguageControl.Get(fName, "1"), arg));
                }
            }
        }

        public static void StopServerDiscovery() {
            if (IsDiscoveryStarted) {
                IsDiscoveryStarted = false;
                DisposePeer();
            }
        }

        public static void Update() {
            if (IsDiscoveryStarted && Time.FrameStartTime >= LastLocalDiscoveryTime + LocalDiscoveryPeriod) {
                LastLocalDiscoveryTime = Time.FrameStartTime;
                SendLocalDiscoveryRequest();
            }
            if (IsDiscoveryStarted && Time.FrameStartTime >= LastInternetDiscoveryTime + InternetDiscoveryPeriod) {
                LastInternetDiscoveryTime = Time.FrameStartTime;
                Task.Run(
                    delegate {
                        List<IPEndPoint> addresses = DnsQueryServerAddresses();
                        Dispatcher.Dispatch(delegate { SendInternetDiscoveryRequests(addresses); });
                    }
                );
            }
            if (Time.PeriodicEvent(0.25, 0.0)) {
                publicDiscoveredServers.RemoveAll(s => Time.FrameStartTime > s.DiscoveryTime + (s.IsLocal ? 3 : 7));
            }
        }

        public static List<IPEndPoint> DnsQueryServerAddresses() {
            lock (ServerAddresses) {
                _ = Time.RealTime;
                if (ServerAddresses.Count == 0) {
                    Task<HashSet<IPEndPoint>>[] array = new Task<HashSet<IPEndPoint>>[SettingsManager.LobbyServers.Count];
                    for (int i = 0; i < SettingsManager.LobbyServers.Count; i++) {
                        string name = SettingsManager.LobbyServers[i];
                        array[i] = Task.Run(
                            delegate {
                                HashSet<IPEndPoint> list = [];
                                try {
                                    IPAddress[] addressList = Dns.GetHostEntry(name).AddressList;
                                    foreach (IPAddress iPAddress in addressList) {
                                        if (iPAddress.AddressFamily == AddressFamily.InterNetwork
                                            || iPAddress.AddressFamily == AddressFamily.InterNetworkV6) {
                                            list.Add(new IPEndPoint(iPAddress, 40102));
                                        }
                                    }
                                }
                                catch {
                                    // ignored
                                }
                                return list;
                            }
                        );
                    }
                    Task.WaitAll(array.Cast<Task>().ToArray(), 5000);
                    for (int i = 0; i < array.Length; i++) {
                        Task<HashSet<IPEndPoint>> task = array[i];
                        if (task.IsCompleted) {
                            HashSet<IPEndPoint> result = task.Result;
                            string name = SettingsManager.LobbyServers[i];
                            foreach (IPEndPoint endPoint in result) {
                                ServerAddresses.Add(endPoint, name);
                            }
                        }
                    }
                    Log.Information(LanguageControl.Get(fName, "2") + ServerAddresses.Aggregate("", (s, a) => s + a + " "));
                }
                return ServerAddresses.Keys.ToList();
            }
        }

        public static void CreatePeer() {
            DisposePeer();
            Peer = new Peer(new DiagnosticTransmitter(new UdpTransmitter()));
            Peer.Error += delegate(Exception e) {
                if (e is ProtocolViolationException) {
                    Log.Warning(e);
                }
                else {
                    Log.Error(e);
                }
            };
            Peer.PeerDiscovered += delegate(Packet p) {
                if (p.Bytes.Length >= 5
                    && p.Bytes[0] == 0) {
                    Version version = new(BitConverter.ToInt32(p.Bytes, 1));
                    if (version.GetNetworkProtocolVersion() > VersionsManager.Version.GetNetworkProtocolVersion()
                        && (!NewVersionServerDiscovered.HasValue || version > NewVersionServerDiscovered.Value)) {
                        NewVersionServerDiscovered = version;
                    }
                }
                else {
                    if (!(Message.Read(p.Bytes) is GameListMessage message)) {
                        throw new ProtocolViolationException(LanguageControl.Get(fName, "3"));
                    }
                    Handle(message, p.Address);
                }
            };
            Peer.Start();
        }

        public static void DisposePeer() {
            Peer peer = Peer;
            Peer = null;
            Task.Run(delegate { peer?.Dispose(); });
        }

        public static void SendLocalDiscoveryRequest() {
            if (Peer != null) {
                Peer.DiscoverLocalPeers(40102, VersionsManager.Version.ToByteArray());
                GameListLocalRequestTime = Time.RealTime;
            }
        }

        public static void SendInternetDiscoveryRequests(IEnumerable<IPEndPoint> addresses) {
            if (Peer == null) {
                return;
            }
            foreach (IPEndPoint address in addresses) {
                Peer.DiscoverPeer(address, VersionsManager.Version.ToByteArray());
                GameListRequestTimes[address] = Time.RealTime;
            }
        }

        public static void Handle(GameListMessage message, IPEndPoint address) {
            bool isLocal = false;
            float ping = float.PositiveInfinity;
            if (GameListRequestTimes.TryGetValue(address, out double value)) {
                ping = (float)(Time.RealTime - value);
                //isLocal = false;
            }
            else if (GameListLocalRequestTime != 0.0) {
                ping = (float)(Time.RealTime - GameListLocalRequestTime);
                isLocal = true;
            }
            Dispatcher.Dispatch(
                delegate {
                    publicDiscoveredServers.RemoveAll(sd => Equals(sd.Address, address));
                    ServerDescription discoveredServer = new() {
                        Address = address,
                        IsLocal = isLocal,
                        DiscoveryTime = Time.RealTime,
                        Ping = ping,
                        Priority = message.ServerPriority,
                        Name = message.ServerName
                    };
                    discoveredServer.GameDescriptions.AddRange(
                        message.GameDescriptions.Select(
                            delegate(GameDescription gd) {
                                gd.ServerDescription = discoveredServer;
                                return gd;
                            }
                        )
                    );
                    publicDiscoveredServers.Add(discoveredServer);
                }
            );
        }
    }
}